<?php

namespace Fluency\Web;

use Fluency\Web\Http\ActiveRequest;
use Fluency\Web\Http\Response;
use Fluency\Web\Http\TerminatingResponse;

/**
 * An immutable representation of a chain of filters to be applied to an action. It executes the
 * given filters before the action, but also allows them to return continuations, which are
 * executed after the action in an order opposite to the one they are returned in.
 *
 * @package Fluency\Web
 * @author  Ignas Rudaitis
 * @since   1.0
 */
class FilterChain
{
    private $filters;
    private $action;

    /**
     * The constructor.
     *
     * @param callable[] $filters the filters to be applied to the action
     * @param callable $action
     */
    public function __construct(array $filters, $action)
    {
        $this->filters = $filters;
        $this->action = $action;
    }

    private function executeFilters(array $filters, ActiveRequest $req, array &$continuations)
    {
        $continuationQueue = array();
        foreach ($filters as $offset => $filter) {
            $result = $filter($req);
            if (is_callable($result)) {
                $continuationQueue[$offset] = $result;
            } elseif ($result instanceof Response) {
                return $result;
            } elseif ($result !== null) {
                throw new \UnexpectedValueException(
                        "Filter at offset '$offset' returned a value of type '" .
                        gettype($result) . "' when it should return either NULL, " .
                        "a Fluency\\Web\\Http\\Response, or a callable (a filter continuation)");
            }
        }
        $continuations = array_reverse($continuationQueue, true);
    }

    private function executeFilterContinuations(array $continuations, $response)
    {
        foreach ($continuations as $offset => $filter) {
            $response = $filter($response);
            if ($response instanceof TerminatingResponse) {
                return $response;
            } elseif (!$response instanceof Response) {
                throw new \UnexpectedValueException(
                        "Continuation of filter at offset '$offset' did not return a " .
                        "Fluency\\Web\\Http\\Response");
            }
        }
        return $response;
    }

    /**
     * Executes the filter chain and returns the final response.
     *
     * @param Fluency\Web\Http\ActiveRequest $req the request to which the action should respond
     * @return Fluency\Web\Http\Response
     */
    public function __invoke(ActiveRequest $req)
    {
        $continuations = array();
        $prematureResponse = $this->executeFilters($this->filters, $req, $continuations);
        if ($prematureResponse !== null) {
            return $prematureResponse;
        }
        $action = $this->action;
        $response = $action($req);
        if ($response instanceof TerminatingResponse) {
            return $response;
        }
        return $this->executeFilterContinuations($continuations, $response);
    }
}
